package plotterTool;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.util.HashMap;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.swing.JPanel;


/**
 * The output panel is the graph. 
 * 
 * Simple drawing of a graph based on a JavaScript formula.
 * 
 * OutputPanel uses a HashMap<String name, Property property> of Properties to build
 * a graph. The properties contains information about how the axis should be, and
 * also the formula itself. 
 * 
 * @author Knut
 */
class OutputPanel extends JPanel {
	private static final long serialVersionUID = 1L;

	/**
	 * Resolution
	 * (the graph was originally written with a hard-coded resolution of 100,
	 * which gave a ugly output. The values are now replaced with these
	 * constants)
	 */
	private static int RES = 500;
	
	/**
	 * Margin from the borders and in to the start of the graph axis.
	 */
	private static int MARGIN = 20;
	private static int LOW = MARGIN;
	private static int HIGH = RES-MARGIN;
	private static int INNER = RES-2*MARGIN;
	
	/**
	 * JavaScript engine for evaluating the mathematical formula:
	 */
	ScriptEngine javascript = new ScriptEngineManager().getEngineByName("JavaScript");
	
	/**
	 * The properties of this graph.
	 */
	public HashMap<String, Property> properties = new HashMap<String, Property>();
	
	/**
	 * Sets default properties and prepares the JavaScript engine.
	 */
	public OutputPanel() {
		setPreferredSize(new Dimension(500, 550));
		
		properties.put("xfrom", new Property(Property.Type.INT, "X-akse fra", "-5"));
		properties.put("xto", new Property(Property.Type.INT, "X-akse til", "5"));
		properties.put("yfrom", new Property(Property.Type.INT, "Y-akse fra", "-5"));
		properties.put("yto", new Property(Property.Type.INT, "Y-aske til", "5"));
		properties.put("steplength", new Property(Property.Type.INT, "Steglengde", "1"));
		
		properties.put("formula", new Property(Property.Type.STRING, "Y=", "sin(x)-cos(0.5*x)"));
		
		
		/*
		 * Defining some of the important functions in global scope:
		 */
		try {
			javascript.eval("var sin = Math.sin; var cos = Math.cos; var tan = Math.tan;"+
							"var pi = Math.PI; var e = Math.E;" +
							"var round = Math.round; var pow = Math.pow; var abs = Math.abs;" +
							"function sign(input) { if (input > 0) return 1; else if (input == 0) return 0; else return -1; }");
		} catch (ScriptException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Paint the graph. The evaluation of one JavaScript expression for
	 * each unit of the resolution slows down the drawing process. 
	 */
	public void paintComponent(Graphics _g) {
		Graphics2D g = (Graphics2D)_g;
	
		// Fill with white background:
		g.setColor(Color.white);
		g.fillRect(0,  0, getWidth(), getHeight());
		
		// Save the original transformation:
		AffineTransform originalTransform = g.getTransform();
		g.scale(500/RES, 500/RES);
		
		g.setColor(Color.black);

		// Retrieve some properties:
		double xFrom = getDoubleProperty("xfrom");
		double xTo = getDoubleProperty("xto");
		double xDiff = xTo-xFrom;
		
		double yFrom = getDoubleProperty("yfrom");
		double yTo = getDoubleProperty("yto");
		double yDiff = yTo-yFrom;
		
		// Steplength = 0 causes a infinity loop, so:
		int steplength = getIntProperty("steplength");
		if (steplength < 1) {
			steplength = 1;
		}
		
		// The font used on the axis.
		g.setFont(new Font(Font.MONOSPACED, 0, 3*RES/100));
		
		/*
		 * Draw the X-axis. First: detect where on the y-axis the x-axis
		 * should be located. If "outside" the y-axis (0 does not exist on the
		 * y-axis), the axis should be painted in gray.
		 */
		g.setColor(Color.black);
		int xAxisCenter;
		if (yFrom > 0) {
			g.setColor(Color.gray);
			xAxisCenter = HIGH;
		} else if (yTo < 0) {
			g.setColor(Color.gray);
			xAxisCenter = LOW;
		} else {
			xAxisCenter = LOW+(int)((yDiff+yFrom)/yDiff*INNER);			
		}
		g.drawLine(LOW, xAxisCenter, HIGH, xAxisCenter);
		
		// Draw the units on the x-axis and the labels:
		for (int i = 0; i < xDiff+1; i=i+steplength) {
			g.drawLine((int)(LOW+(i/(xDiff)*INNER)), xAxisCenter-(2*RES/100), (int)(LOW+(i/(xDiff)*INNER)), xAxisCenter+(RES/100));
			g.drawString(""+(int)(i+xFrom), (int)(LOW+(i/(xDiff)*INNER)), xAxisCenter+(4*RES/100));
		}
		
		// Draw y-axis:
		g.setColor(Color.black);
		int yAxisCenter;
		if (xFrom > 0) {
			g.setColor(Color.gray);
			yAxisCenter = LOW;
		} else if (xTo < 0) {
			g.setColor(Color.gray);
			yAxisCenter = HIGH;
		} else {
			yAxisCenter = LOW+(int)((xDiff-xTo)/xDiff*INNER);			
		}
		g.drawLine(yAxisCenter, LOW, yAxisCenter, HIGH);
		
		// Units and labels on y-axis:
		for (int i = 0; i < yDiff+1; i=i+steplength) {
			g.drawLine(yAxisCenter-(RES/100), (int)(LOW+(i/(yDiff)*INNER)), yAxisCenter+(2*RES/100), (int)(LOW+(i/(yDiff)*INNER)));
			g.drawString(""+(int)(yTo-i), yAxisCenter-(4*RES/100), (int)(LOW+(i/(yDiff)*INNER)));
		}
		
		////////////////////////////////////////////////////
		//
		// SECURITY NOTE!!
		//
		// The user may enter dangerous scripts that may
		// harm the computer. The formula input is
		// evaluated as JavaScript with unlimited
		// permissions to the computer.
		//
		// Removing ; and " and ' makes it hard to enter
		// multiple commands or eval() commands, but
		// there may still be ways to harm the computer!
		//
		////////////////////////////////////////////////////
		
		
		// Draw the graph: (if the formula is not valid, the try-clause fails)
		g.setColor(Color.blue);
		g.setStroke(new BasicStroke(2));
		try {	
			String formula = getStringProperty("formula").replace('\'', ' ').replace('"', ' ').replace(';', ' ');
			
			Integer prevX = null;
			Integer prevY = null;
			
			for (int i = LOW; i < HIGH; i++) {
				double x = ((double)(i-LOW)/INNER*xDiff)+xFrom; // The x-value at this position.
			
				javascript.put("x", x);
				javascript.put("X", x);
				
				double y = (Double) javascript.eval(formula);
				int j = (int)(LOW+((yDiff-y+yFrom)/yDiff*INNER));
				
				if (prevX != null && prevY != null) {
					g.drawLine(prevX, prevY, i, j);
				}
				prevX = i;
				prevY = j;
			}
			
			g.drawString("y = " + formula, LOW, RES+LOW);
		} catch (Exception e) {
			
		}
		
		
		// Set back to original transformation. (only needed if there exists SWING components on top of the panel.)
		g.setTransform(originalTransform);
	}
	
	/**
	 * Retrieve a property as a double.
	 * @param name
	 * @return
	 */
	public double getDoubleProperty(String name) {
		return Double.parseDouble(properties.get(name).value);
	}
	
	/**
	 * Retrieve a property as a string.
	 * @param name
	 * @return
	 */
	public String getStringProperty(String name) {
		return properties.get(name).value;
	}
	
	/**
	 * Retrieve a property as a int
	 * @param name
	 * @return
	 */
	public int getIntProperty(String name) {
		return Integer.parseInt(properties.get(name).value);
	}
}